﻿#if !NO_RECORDS
#nullable enable
using NBitcoin.DataEncoders;
using NBitcoin.Scripting;
using NBitcoin.WalletPolicies.Visitors;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using static NBitcoin.WalletPolicies.MiniscriptError;
using static NBitcoin.WalletPolicies.MiniscriptNode;
using static NBitcoin.WalletPolicies.MiniscriptNode.Parameter;
using static NBitcoin.WalletPolicies.MiniscriptNode.Value;

namespace NBitcoin.WalletPolicies
{
	public enum KeyType
	{
		Classic,
		Taproot
	}

	/// <summary>
	/// How to interprete the script
	/// </summary>
	public enum MiniscriptDialect
	{
		/// <summary>
		/// Only allow miniscript policy standard fragments
		/// </summary>
		Strict,
		/// <summary>
		/// Requires BIP388 fragments (tr, pkh, wpkh, sh, wsh) as top level fragments. Also allow the use of checksum.
		/// </summary>
		BIP388
	}

	[Flags]
	public enum ParameterTypeFlags
	{
		None = 0,
		/// <summary>
		/// Allow the use of multipath's key placeholders (eg. @0/**)
		/// </summary>
		KeyPlaceholder = 1,
		/// <summary>
		/// Allow the use of named parameter (eg. pkh(Alice))
		/// </summary>
		NamedParameter = 2,
		/// <summary>
		/// Allow both key and name placeholders
		/// </summary>
		All = 3
	}
	public class MiniscriptParsingSettings
	{
		/// <summary>
		/// 
		/// </summary>
		/// <param name="network"></param>
		/// <param name="defaultKeyType">See <see cref="KeyType"/> for details on how key type ambiguities are resolved.</param>
		public MiniscriptParsingSettings(Network network, KeyType? defaultKeyType = null)
		{
			ArgumentNullException.ThrowIfNull(network);
			Network = network;
			KeyType = defaultKeyType;
		}

		/// <inheritdoc cref="MiniscriptDialect"/>
		public MiniscriptDialect Dialect { get; set; }

		/// <summary>
		/// Whether key placeholders or named parameters are allowed. (Default to <see cref="ParameterTypeFlags.KeyPlaceholder"/>)
		/// </summary>
		public ParameterTypeFlags AllowedParameters { get; set; } = ParameterTypeFlags.KeyPlaceholder;
		public Network Network { get; }
		/// <summary>
		/// How to solve ambiguities of key types. For example, with <c>PK(A)</c>, is <c>A</c> a taproot key or ecdsa key? 
		/// Note that if <see cref="Dialect"/> is set to <see cref="MiniscriptDialect.BIP388"/>, this parameter is ignored because no ambiguity is possible. 
		/// The default behavior is to throw an error when parsed if there is an ambiguity.
		/// </summary>
		public KeyType? KeyType { get; set; }

	}

	/// <summary>
	/// A low level representation of a miniscript script.
	/// Use <see cref="WalletPolicy"/> if you just want to parse a descriptor as generated by wallets supporting BIP388.
	/// </summary>
	public class Miniscript
	{
		/// <summary>
		/// Scripts generated by the Miniscript
		/// </summary>
		/// <param name="ScriptPubKey">The script that appears in the <see cref="TxOut.ScriptPubKey"></see></param>
		/// <param name="RedeemScript">The script that must be pushed in the <see cref="TxIn.WitScript"></see> for WSH outputs, or <see cref="TxIn.ScriptSig"></see> for non-segwit P2SH outputs</param>
		/// <param name="ScriptCode">The script that must be included in the hash algorithm used for signing the input.</param>
		public record Scripts(Script ScriptPubKey, Script? RedeemScript, Script? ScriptCode);
		public record TaprootInfo(TaprootInternalPubKey InternalPubKey, uint256? MerkleRoot);
		public MiniscriptNode RootNode { get; }
		public IReadOnlyDictionary<string, IReadOnlyCollection<MiniscriptNode.Parameter>> Parameters { get; }

		public Network Network { get; }

		internal Miniscript(MiniscriptNode rootNode, Network network, KeyType keyType)
		{
			if (network is null)
				throw new ArgumentNullException(nameof(network));
			if (rootNode is null)
				throw new ArgumentNullException(nameof(rootNode));
			Network = network;
			RootNode = rootNode;
			if (!ParametersVisitor.TryCreateParameters(rootNode, out var error, out var parameters))
				throw new MiniscriptFormatException(error);
			Parameters = parameters;
			KeyType = keyType;
		}
		Miniscript(
			MiniscriptNode rootNode,
			IReadOnlyDictionary<string, IReadOnlyCollection<MiniscriptNode.Parameter>> parameters,
			Network network,
			KeyType keyType
			)
		{
			RootNode = rootNode;
			Parameters = parameters;
			Network = network;
			KeyType = keyType;
		}
		public KeyType KeyType { get; }
		public override string ToString() => ToString(false);

		public static bool TryParse(string str, Network network, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out Miniscript miniscript) => TryParse(str, new MiniscriptParsingSettings(network), out error, out miniscript);
		public static bool TryParse(string str, MiniscriptParsingSettings settings, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out Miniscript miniscript)
		{
			ArgumentNullException.ThrowIfNull(str);
			if (TryParseMiniscript(new ParsingContext(str, settings), out error, out miniscript))
				return true;
			miniscript = null;
			return false;
		}
		public static bool TryParse(string str, Network network, [MaybeNullWhen(false)] out Miniscript miniscript) => TryParse(str, new MiniscriptParsingSettings(network), out miniscript);
		public static bool TryParse(string str, MiniscriptParsingSettings settings, [MaybeNullWhen(false)] out Miniscript miniscript)
		{
			return TryParse(str, settings, out _, out miniscript);
		}
		public static Miniscript Parse(string str, Network network) => Parse(str, new MiniscriptParsingSettings(network));
		public static Miniscript Parse(string str, MiniscriptParsingSettings settings)
		{
			ArgumentNullException.ThrowIfNull(str);
			if (TryParseMiniscript(new ParsingContext(str, settings), out var error, out var miniscript))
				return miniscript;
			throw new MiniscriptFormatException(error);
		}

		delegate bool Parsing(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node);


		private static bool TryParseExpressions(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node)
		{
			node = null;
			error = null;
			if (ctx.CurrentFrame.Parameters.Count == 0)
			{
				if (!TryParseParameterCount(ctx, out error, out node))
					return false;
				if (node is not Value.CountValue count)
					return true;
				ctx.CurrentFrame.ExpectedParameterCount = count.Count + 1;
				return true;
			}
			else
			{
				if (ctx.CurrentFrame.ExpectedParameterCount == ctx.CurrentFrame.Parameters.Count + 1)
					ctx.CurrentFrame.ExpectedParameterCount = -1;
				return TryParseExpression(ctx, out error, out node);
			}
		}
		private static bool TryParsePubKeys<T>(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node)
		{
			node = null;
			error = null;

			var expectedKeyType = typeof(T) == typeof(Value.PubKeyValue) ? KeyType.Classic : KeyType.Taproot;

			if (!ctx.TrySetExpectedKeyType(expectedKeyType))
			{
				error = new MiniscriptError.UnsupportedFragment(ctx.FragmentIndex, ctx.ExpectedKeyType!.Value);
				return false;
			}

			if (ctx.CurrentFrame.Parameters.Count == 0)
			{
				if (!TryParseParameterCount(ctx, out error, out node))
					return false;
				if (node is not Value.CountValue count)
					return true;
				ctx.CurrentFrame.ExpectedParameterCount = count.Count + 1;
				return true;
			}
			else
			{
				if (ctx.CurrentFrame.ExpectedParameterCount == ctx.CurrentFrame.Parameters.Count + 1)
					ctx.CurrentFrame.ExpectedParameterCount = -1;
				return TryParseKey(ctx, out error, out node);
			}
		}
		private static bool TryParseParameterCount(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node)
		{
			error = null;
			node = null;
			var match = Regex.Match(ctx.Remaining, @"^[0-9]+");
			if (uint.TryParse(match.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) && v < Int32.MaxValue)
			{
				ctx.Advance(match.Length);
				node = new Value.CountValue((int)v);
				return true;
			}
			error = new MiniscriptError.CountExpected(ctx.Offset);
			return false;
		}
		private static bool TryParse32Bytes(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node) => TryParseBytes(ctx, 32, out error, out node);
		private static bool TryParse20Bytes(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node) => TryParseBytes(ctx, 20, out error, out node);
		private static bool TryParseBytes(ParsingContext ctx, int requiredBytes, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node)
		{
			node = null;
			error = null;
			var match = Regex.Match(ctx.Remaining, @"^[a-f0-9]{17,}");
			if (match.Success && match.Length == requiredBytes * 2)
			{
				ctx.Advance(match.Length);
				node = new Value.HashValue(Encoders.Hex.DecodeData(match.Value));
				return true;
			}

			if (TryParseNamedParameter(ctx, new ParameterRequirement.Hash(requiredBytes), out node))
				return true;

			error = new MiniscriptError.HashExpected(ctx.Offset, requiredBytes);
			return false;
		}

		private static bool TryParseLocktime(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node)
		{
			node = null;
			error = null;
			var match = Regex.Match(ctx.Remaining, @"^[0-9]+");
			if (uint.TryParse(match.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v))
			{
				ctx.Advance(match.Length);
				var locktime = new LockTime(v);
				node = new Value.LockTimeValue(locktime);
				return true;

			}
			if (TryParseNamedParameter(ctx, ParameterRequirement.Locktime.Instance, out node))
				return true;
			error = new MiniscriptError.LocktimeExpected(ctx.Offset);
			return false;
		}

		private static bool TryParseNamedParameter(ParsingContext ctx, ParameterRequirement requirement, [MaybeNullWhen(false)] out MiniscriptNode node)
		{
			static bool IsForbiddenName(string name) => name switch
			{
				"0" or "1" or "pk_k" or "pk_h" or "pk" or "pkh" or "older" or "after" or "sha256" or "ripemd160" or "hash256" or "hash160" or "andor" or "and_v" or "and_b" or "and_n" or "or_b" or "or_c" or "or_d" or "or_i" or "thresh" or "multi" or "multi_a" or "musig" => true,
				"wsh" or "sh" or "tr" => true,
				_ => false
			};
			node = null;
			if (ctx.ParsingSettings.AllowedParameters == ParameterTypeFlags.None)
				return false;
			var key = Regex.Match(ctx.Remaining, @"^([a-zA-Z0-9_]+)|(@[0-9]{1,4})");
			if (key is { Success: true, Length: <= 16 * 2 } && !IsForbiddenName(key.Value))
			{
				if (!key.Value.StartsWith('@') && !ctx.ParsingSettings.AllowedParameters.HasFlag(ParameterTypeFlags.NamedParameter))
					return false;
				if (key.Value.StartsWith('@') && !ctx.ParsingSettings.AllowedParameters.HasFlag(ParameterTypeFlags.KeyPlaceholder))
					return false;
				ctx.Advance(key.Value.Length);
				node = new Parameter(key.Value, requirement);
				return true;
			}
			return false;
		}

		internal static bool TryParseKey(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node)
		{
			node = null;
			error = null;
			ctx.SkipSpaces();
			var key = Regex.Match(ctx.Remaining, @"^[a-f0-9]+");
			if (key is { Success: true, Length: (33 * 2 or (65 * 2) or 32 * 2) })
			{
				ctx.Advance(key.Length);
				try
				{
					node = (key.Length, ctx.ExpectedKeyType) switch
					{
						(33 * 2 or 65 * 2, null or KeyType.Classic) => new Value.PubKeyValue(new PubKey(Encoders.Hex.DecodeData(key.Value))),
						(32 * 2, null or KeyType.Taproot) => new Value.TaprootPubKeyValue(new TaprootPubKey(Encoders.Hex.DecodeData(key.Value))),
						_ => null
					};
					if (node != null)
						return true;
				}
				catch
				{
					error = new MiniscriptError.KeyExpected(ctx.Offset, ctx.ExpectedKeyType);
					return false;
				}
			}

			if (node is null)
			{
				TryParseNamedParameter(ctx, new ParameterRequirement.Key(ctx.ExpectedKeyType), out node);
			}

			if (node is null && MusigNode.IsMusig(ctx))
				if (!MusigNode.TryParse(ctx, out node, out error))
					return false;

			if (node is Parameter { IsKeyPlaceholder: true } or MusigNode or null)
			{
				if (node is null)
					HDKeyNode.HDKeyNode.TryParse(ctx, out node);
				if (node is not null && ctx.Peek('/', out _))
				{
					if (node is Parameter p)
						node = p with { Requirement = new ParameterRequirement.HDKey() };
					if (!MultipathNode.TryParseMultiPath(ctx, node, out var multiPathNode, out error))
						return false;
					node = multiPathNode;
				}
			}

			if (node is null && !TryParseBase58(ctx, out node))
			{
				error = new MiniscriptError.KeyExpected(ctx.Offset, ctx.ExpectedKeyType);
				return false;
			}

			return true;
		}

		private static bool TryParseBase58(ParsingContext ctx, [MaybeNullWhen(false)] out MiniscriptNode node)
		{
			node = null;
			if ((ctx.ExpectedKeyType ?? ctx.DefaultKeyType) is not { } keyType)
				return false;
			var match = Regex.Match(ctx.Remaining, @"^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{50,}");
			if (match.Success)
			{
				try
				{
					var result = ctx.Network.Parse(match.Value);
					if (result is IHDKey hdkey)
					{
						node = new HDKeyValue(hdkey, keyType);
					}
					if (result is BitcoinSecret secret)
					{
						node = new Value.PrivateKeyValue(secret);
					}
					if (node is not null)
					{
						ctx.Advance(match.Length);
						return true;
					}
				}
				catch
				{
					return false;
				}
			}
			return false;
		}

		private static bool TryParseMiniscript(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out Miniscript miniscript)
		{
			error = null;
			miniscript = null;
			MiniscriptNode? node = null;
			ctx.SkipSpaces();

			if (ctx.ParsingSettings.Dialect == MiniscriptDialect.BIP388)
			{
				var match = Regex.Match(ctx.Remaining, "^[a-zA-Z0-9_]+");
				if (!match.Success)
				{
					error = ctx.IsEnd ? new IncompleteExpression(ctx.Offset) : new UnexpectedToken(ctx.Offset);
					return false;
				}
				var keyType = match.ValueSpan switch
				{
					"tr" => KeyType.Taproot,
					_ => KeyType.Classic
				};
				ctx.ExpectedKeyType = keyType;

				var descriptor = match.ValueSpan switch
				{
					"wpkh" => FragmentDescriptor.wpkh,
					"pkh" => FragmentDescriptor.pkh,
					"tr" => FragmentDescriptor.tr,
					"wsh" => FragmentDescriptor.wsh,
					"sh" => FragmentDescriptor.sh,
					_ => null
				};
				using (var frame = ctx.PushFrame())
				{
					frame.FragmentIndex = ctx.Offset;
					ctx.Advance(match.Index + match.Length);

					if (!ctx.Peek('(', out error))
						return false;
					if (descriptor is null)
					{
						error = new MiniscriptError.InvalidTopFragment(ctx.Offset);
						return false;
					}

					if (descriptor == FragmentDescriptor.wpkh || descriptor == FragmentDescriptor.pkh)
					{
						node = TryParseParameters(ctx, 1, TryParseKey, out error, out var p) ? new FragmentSingleParameter(descriptor, p[0]) : null;
					}
					else if (ctx.Network.Consensus.SupportTaproot && descriptor == FragmentDescriptor.tr)
					{
						node = TryParseTaproot(ctx, out error, out var n) ? n : null;
					}
					else if (ctx.Network.Consensus.SupportSegwit && descriptor == FragmentDescriptor.wsh)
					{
						node = TryParseParameters(ctx, 1, TryParseExpression, out error, out var p) ? new FragmentSingleParameter(descriptor, p[0]) : null;
					}
					else if (descriptor == FragmentDescriptor.sh)
					{
						var prevOffset = ctx.Offset;
						ctx.Advance(1);
						ctx.SkipSpaces();
						match = Regex.Match(ctx.Remaining, "^((wsh)|(wpkh))");
						if (ctx.Network.Consensus.SupportSegwit && match.Success)
						{
							using (var frame2 = ctx.PushFrame())
							{
								frame2.FragmentIndex = ctx.Offset;
								ctx.Advance(match.Index + match.Length);
								var wrappedDescriptor = match.ValueSpan switch
								{
									"wpkh" => FragmentDescriptor.wpkh,
									"wsh" => FragmentDescriptor.wsh,
									// Can never happen
									_ => throw new NotSupportedException()
								};
								node = match.ValueSpan switch
								{
									"wpkh" => TryParseParameters(ctx, 1, TryParseKey, out error, out var p) ? new FragmentSingleParameter(wrappedDescriptor, p[0]) : null,
									"wsh" => TryParseParameters(ctx, 1, TryParseExpression, out error, out var p) ? new FragmentSingleParameter(wrappedDescriptor, p[0]) : null,
									_ => null
								};
								if (node is not null)
								{
									node = new FragmentSingleParameter(descriptor, node);
									ctx.Advance(1);
								}
							}
						}
						else
						{
							ctx.Offset = prevOffset;
							node = TryParseParameters(ctx, 1, TryParseExpression, out error, out var p) ? new FragmentSingleParameter(descriptor, p[0]) : null;
						}
					}
				}
			}
			else
			{
				TryParseExpression(ctx, out error, out node);
			}

			ctx.ExpectedKeyType ??= ctx.DefaultKeyType;
			if (node is not null &&
				ParametersVisitor.TryCreateParameters(node, out error, out var parameters) &&
				ctx.ExpectedKeyType is not null)
			{
				ctx.SkipSpaces();
				if (!ctx.IsEnd && ctx.ParsingSettings.Dialect == MiniscriptDialect.BIP388)
				{
					if (ctx.NextChar == '#')
					{
						var actualChecksum = ctx.Remaining[1..Math.Min(9, ctx.RemainingChars)];
						var expectedChecksum = OutputDescriptor.GetCheckSum(ctx.Miniscript[0..ctx.Offset]);
						if (expectedChecksum != actualChecksum)
						{
							error = new MiniscriptError.InvalidChecksum(ctx.Offset);
							return false;
						}
						ctx.Advance(9);
						ctx.SkipSpaces();
					}

				}
				if (!ctx.IsEnd)
				{
					error = new MiniscriptError.UnexpectedToken(ctx.Offset);
					return false;
				}
				miniscript = new Miniscript(node, parameters, ctx.Network, ctx.ExpectedKeyType.Value);
				return true;
			}

			if (ctx.ExpectedKeyType is null)
				error ??= new AmbiguousKeyType();
			error ??= new IncompleteExpression(ctx.Offset);
			miniscript = null;
			return false;
		}

		private static bool TryParseExpression(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node)
		{
			node = null;
			error = null;

			ctx.SkipSpaces();

			int endOffset = ctx.Offset;
			var match = Regex.Match(ctx.Remaining, @"^([a-z0-9_]+?:)?([a-zA-Z0-9_]+)");

			var wrapperGroup = match.Groups[1];
			var fragmentName = match.Groups[2];

			if (fragmentName.Success)
			{
				using var frame = ctx.PushFrame();
				frame.FragmentIndex = ctx.Offset + fragmentName.Index;
				ctx.Advance(fragmentName.Index + fragmentName.Length);
				node = fragmentName.Value switch
				{
					"0" => FragmentNoParameter._0,
					"1" => FragmentNoParameter._1,
					"pk_k" => TryParseParameters(ctx, 1, TryParseKey, out error, out var p) ? FragmentSingleParameter.pk_k(p[0]) : null,
					"pk_h" => TryParseParameters(ctx, 1, TryParseKey, out error, out var p) ? FragmentSingleParameter.pk_h(p[0]) : null,
					"pk" => TryParseParameters(ctx, 1, TryParseKey, out error, out var p) ? FragmentSingleParameter.pk(p[0]) : null,
					"pkh" => TryParseParameters(ctx, 1, TryParseKey, out error, out var p) ? FragmentSingleParameter.pkh(p[0]) : null,
					"older" => TryParseParameters(ctx, 1, TryParseLocktime, out error, out var p) ? FragmentSingleParameter.older(p[0]) : null,
					"after" => TryParseParameters(ctx, 1, TryParseLocktime, out error, out var p) ? FragmentSingleParameter.after(p[0]) : null,
					"sha256" => TryParseParameters(ctx, 1, TryParse32Bytes, out error, out var p) ? FragmentSingleParameter.sha256(p[0]) : null,
					"ripemd160" => TryParseParameters(ctx, 1, TryParse20Bytes, out error, out var p) ? FragmentSingleParameter.ripemd160(p[0]) : null,
					"hash256" => TryParseParameters(ctx, 1, TryParse32Bytes, out error, out var p) ? FragmentSingleParameter.hash256(p[0]) : null,
					"hash160" => TryParseParameters(ctx, 1, TryParse20Bytes, out error, out var p) ? FragmentSingleParameter.hash160(p[0]) : null,
					"andor" => TryParseParameters(ctx, 3, TryParseExpression, out error, out var p) ? FragmentThreeParameters.andor(p[0], p[1], p[2]) : null,
					"and_v" => TryParseParameters(ctx, 2, TryParseExpression, out error, out var p) ? FragmentTwoParameters.and_v(p[0], p[1]) : null,
					"and_b" => TryParseParameters(ctx, 2, TryParseExpression, out error, out var p) ? FragmentTwoParameters.and_b(p[0], p[1]) : null,
					"and_n" => TryParseParameters(ctx, 2, TryParseExpression, out error, out var p) ? FragmentTwoParameters.and_n(p[0], p[1]) : null,
					"or_b" => TryParseParameters(ctx, 2, TryParseExpression, out error, out var p) ? FragmentTwoParameters.or_b(p[0], p[1]) : null,
					"or_c" => TryParseParameters(ctx, 2, TryParseExpression, out error, out var p) ? FragmentTwoParameters.or_c(p[0], p[1]) : null,
					"or_d" => TryParseParameters(ctx, 2, TryParseExpression, out error, out var p) ? FragmentTwoParameters.or_d(p[0], p[1]) : null,
					"or_i" => TryParseParameters(ctx, 2, TryParseExpression, out error, out var p) ? FragmentTwoParameters.or_i(p[0], p[1]) : null,
					"thresh" => TryParseParameters(ctx, 1, TryParseExpressions, out error, out var p) ? FragmentUnboundedParameters.thresh(p) : null,
					"sortedmulti" => TryParseParameters(ctx, 1, TryParsePubKeys<Value.PubKeyValue>, out error, out var p) ? FragmentUnboundedParameters.sortedmulti(p) : null,
					"multi" => TryParseParameters(ctx, 1, TryParsePubKeys<Value.PubKeyValue>, out error, out var p) ? FragmentUnboundedParameters.multi(p) : null,
					"multi_a" => ctx.Network.Consensus.SupportTaproot && TryParseParameters(ctx, 1, TryParsePubKeys<Value.TaprootPubKeyValue>, out error, out var p) ? FragmentUnboundedParameters.multi_a(p) : null,
					"sortedmulti_a" => ctx.Network.Consensus.SupportTaproot && TryParseParameters(ctx, 1, TryParsePubKeys<Value.TaprootPubKeyValue>, out error, out var p) ? FragmentUnboundedParameters.sortedmulti_a(p) : null,
					_ => null
				};
				if (node is null && error is null)
				{
					if (ctx.IsEnd || (ctx.NextChar != '(' && fragmentName.Length <= 16))
					{
						node = new Parameter(fragmentName.Value, new ParameterRequirement.Fragment());
					}
					else
					{
						error = new UnknownFragmentName(frame.FragmentIndex, fragmentName.Value);
						return false;
					}
				}
			}

			if (node is null)
			{
				error ??= new IncompleteExpression(ctx.Offset);
				return false;
			}

			if (wrapperGroup.Success)
			{
				for (var i = wrapperGroup.Value.Length - 2; i >= 0; i--)
				{
					Wrapper? wrapper =
						wrapperGroup.Value[i] switch
						{
							'a' => Wrapper.a(node),
							'c' => Wrapper.c(node),
							'd' => Wrapper.d(node),
							'j' => Wrapper.j(node),
							'l' => Wrapper.l(node),
							'n' => Wrapper.n(node),
							's' => Wrapper.s(node),
							't' => Wrapper.t(node),
							'u' => Wrapper.u(node),
							'v' => Wrapper.v(node),
							_ => null
						};
					if (wrapper is null)
					{
						error = new MiniscriptError.InvalidWrapper(wrapperGroup.Value[i], ctx.Offset + i);
						return false;
					}
					node = wrapper;
				}
			}
			error = null;
			return true;
		}

		private static bool TryParseTaproot(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node)
		{
			error = null;
			node = null;
			if (!ctx.Consume('(', out error))
				return false;
			if (!TryParseKey(ctx, out error, out var internalKeyParameter))
				return false;

			if (!ctx.Consume(out var c, out error))
				return false;

			if (c == ')')
			{
				node = new TaprootNode(internalKeyParameter, null);
				return true;
			}
			else if (c == ',')
			{
				if (!TryParseTaprootTree(ctx, out error, out var tree))
					return false;
				if (!ctx.Consume(')', out error))
					return false;
				node = new TaprootNode(internalKeyParameter, tree);
				return true;
			}
			else
			{
				error = new UnexpectedToken(ctx.Offset);
				return false;
			}
		}

		private static bool TryParseTaprootTree(ParsingContext ctx, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode node)
		{
			error = null;
			node = null;
			if (!ctx.Peek(out var c, out error))
				return false;
			if (c == '{')
			{
				ctx.Advance(1);
				if (!TryParseTaprootTree(ctx, out error, out var left) ||
					!ctx.Consume(',', out error) ||
					!TryParseTaprootTree(ctx, out error, out var right) ||
					!ctx.Consume('}', out error))
					return false;
				node = new TaprootBranchNode(left, right);
				return true;
			}
			else if (TryParseExpression(ctx, out error, out node))
			{
				return true;
			}
			return false;
		}

		private static bool TryParseParameters(ParsingContext ctx, int reqParams, Parsing parse, [MaybeNullWhen(true)] out MiniscriptError error, [MaybeNullWhen(false)] out MiniscriptNode[] parameters)
		{
			var frame = ctx.CurrentFrame;
			error = null;
			parameters = null;
			if (!ctx.Consume('(', out error))
				return false;

			frame.ExpectedParameterCount = reqParams;
			while (parse(ctx, out error, out var node))
			{
				frame.Parameters.Add(node);
				if (frame.Parameters.Count == frame.ExpectedParameterCount)
				{
					if (!ctx.Consume(')', out error))
					{
						if (error is MiniscriptError.UnexpectedToken)
							error = new TooManyParameters(ctx.Offset, frame.ExpectedParameterCount);
						return false;
					}
					break;
				}
				else
				{
					if (!ctx.Peek(out var c, out error))
						return false;
					if (frame.ExpectedParameterCount == -1 && c == ')')
					{
						ctx.Advance(1);
						break;
					}
					if (c != ',')
					{
						error = new TooFewParameters(ctx.Offset, frame.ExpectedParameterCount);
						return false;
					}
					ctx.Advance(1);
				}
			}
			if (error is not null)
				return false;
			parameters = frame.Parameters.ToArray();
			return true;
		}

		public Miniscript ReplaceParameters(Dictionary<string, MiniscriptNode> values, bool skipRequirementsCheck = false)
		{
			if (Parameters.Count is 0)
				return this;
			var v = new ParameterReplacementVisitor(values);
			v.SkipRequirements = skipRequirementsCheck;
			return this.Rewrite(v);
		}

		/// <summary>
		/// Generate scriptPubKey and redeemScript from the Miniscript
		/// </summary>
		/// <returns>The scriptPubKey and redeemScript</returns>
		/// <exception cref="InvalidOperationException">Impossible to generate a script while parameters haven't been set</exception>
		public Scripts ToScripts()
		{
			if (Parameters.Count > 0)
				throw new InvalidOperationException("Impossible to generate a script while parameters haven't been set. Use ToScriptString() if you want a user-readable representation instead.");
			var scriptPubKey = RootNode.GetScript();
			Script? scriptCode = null;
			Script? redeem = null;
			if (GetScriptCodeNode(RootNode) is { } n)
				scriptCode = n.GetScript();
			if (GetRedeemNode(RootNode) is { } n2)
				redeem = n2.GetScript();
			return new(scriptPubKey, redeem, scriptCode);
		}

		/// <summary>
		/// Generate a user-readable way to view the Bitcoin script generated by the Miniscript.
		/// </summary>
		/// <returns>A user-readable Bitcoin script</returns>
		public string ToScriptString() => new MockScriptVisitor(Network, KeyType).GenerateScript(RootNode);

		/// <summary>
		/// Generate a user-readable way to view the script actually executed (Can be null of taproot)
		/// </summary>
		/// <returns></returns>
		public string? ToScriptCodeString() => GetScriptCodeNode(RootNode) is { } n ? new MockScriptVisitor(Network, KeyType).GenerateScript(n) : null;

		private MiniscriptNode? GetScriptCodeNode(MiniscriptNode node) =>
		node switch
		{
			Fragment f when f.Descriptor == FragmentDescriptor.wpkh => new FragmentSingleParameter(FragmentDescriptor.pkh, f.Parameters.First()),
			Fragment f when f.Descriptor == FragmentDescriptor.wsh => f.Parameters.First(),
			Fragment f when f.Descriptor == FragmentDescriptor.sh => GetScriptCodeNode(f.Parameters.First()),
			Fragment f when f.Descriptor == FragmentDescriptor.tr => null,
			_ => node
		};
		private MiniscriptNode? GetRedeemNode(MiniscriptNode node) =>
		node switch
		{
			FragmentSingleParameter
			{
				Descriptor: { Name: "sh" },
				X: FragmentSingleParameter { Descriptor: { Name: "wsh" } } wsh
			} => wsh.X,
			FragmentSingleParameter { Descriptor: { Name: "wsh" or "sh" } } f => f.X,
			_ => null
		};

		public Miniscript Rewrite(MiniscriptRewriterVisitor rewriterVisitor)
		{
			var newNode = rewriterVisitor.Visit(RootNode);
			return new Miniscript(newNode, Network, KeyType);
		}
		public void Visit(MiniscriptVisitor visitor)
		{
			visitor.Visit(RootNode);
		}

		/// <summary>
		/// Returns the taproot internal key and the merkle root if there is any tapscript.
		/// </summary>
		/// <returns>Null if the root isn't a taproot node</returns>
		public TaprootInfo? GetTaprootInfo()
		{
			if (RootNode is not TaprootNode tn)
				return null;
			var merkleRoot = TaprootMerkleRootVisitor.GetMerkleRoot(tn);
			return new TaprootInfo(tn.GetInternalKey(), merkleRoot);
		}

		/// <summary>
		/// Replace the multi path key expressions in the miniscript by the actual keys.
		/// </summary>
		/// <param name="intent">Whether this is a deposit or change address</param>
		/// <param name="index">The address index</param>
		/// <returns></returns>
		/// <exception cref="ArgumentOutOfRangeException">index should be positive</exception>
		/// <exception cref="InvalidOperationException">The parameter 'keyType' should be set to call this method, as it not possible to guess the keytype to use from the context.</exception>
		public DerivationResult Derive(AddressIntent intent, int index)
		=> Derive(new DeriveParameters(intent, index))[0];

		/// <summary>
		/// Replace the multi path key expressions in the miniscript by the actual keys.
		/// </summary>
		/// <param name="parameters"></param>
		/// <returns>An array of derivation result (one element per index derived)</returns>
		/// <exception cref="InvalidOperationException"></exception>
		public DerivationResult[] Derive(DeriveParameters parameters)
		{
			var visitor = new DeriveVisitor(parameters.Intent, parameters.AddressIndexes, parameters.DervivationCache ?? new(), KeyType);
			return visitor.Derive(RootNode, Network);
		}

		/// <summary>
		/// Make the miniscript more readable by removing HDKeys (such as [fingerprint]xpub/&lt;0;1&gt;/*) by key placeholders (such as @0/&lt;0;1&gt;/*). It is the reverse operation of <see cref="ReplaceKeyPlaceholdersByHDKeys(HDKeyNode[])"/>
		/// </summary>
		/// <returns>The modified <see cref="Miniscript"/></returns>
		public Miniscript ReplaceHDKeysByKeyPlaceholders(out HDKeyNode[] keys)
		{
			var visitor = new ExtractTemplateVisitor();
			var newTree = Rewrite(visitor);
			keys = visitor.HDKeys.ToArray();
			return newTree;
		}
		/// <summary>
		/// Will replace the key placeholders by the actual keys. It is the inverse operation of <see cref="ReplaceHDKeysByKeyPlaceholders(out HDKeyNode[])"/>
		/// </summary>
		/// <param name="keys"></param>
		/// <returns>The modified <see cref="Miniscript"/></returns>
		public Miniscript ReplaceKeyPlaceholdersByHDKeys(HDKeyNode[] keys)
		{
			var visitor = new FillTemplateVisitor(keys);
			return Rewrite(visitor);
		}

		public string ToString(bool checksum)
		{
			var str = RootNode.ToString();
			if (checksum)
				str = OutputDescriptor.AddChecksum(str);
			return str;
		}
	}
	public record MiniscriptError
	{
		public record InvalidChecksum(int Index) : MiniscriptError
		{
			public override string ToString() => $"Invalid checksum at index {Index}.";
		}
		public record InvalidTopFragment(int Index) : MiniscriptError
		{
			public override string ToString() => $"Invalid BIP0388 top fragment at index {Index}. It should be wsh, sh, wpkh, pkh or tr";
		}
		public record CountExpected(int Index) : MiniscriptError
		{
			public override string ToString() => $"Count expected at index {Index}";
		}
		public record MixedParameterType(string parameterName) : MiniscriptError
		{
			public override string ToString() => $"Mixed parameter type '{parameterName}' (The parameter may be reused)";
		}
		public record MixedNetworks : MiniscriptError
		{
			public override string ToString() => "Mixed Networks detected in the expression";
		}
		public record MixedKeyTypes : MiniscriptError
		{
			public override string ToString() => "Mixed KeyTypes detected in the expression";
		}
		public record AmbiguousKeyType : MiniscriptError
		{
			public override string ToString() => $"Ambiguous key type";
		}
		public record IncompleteExpression(int Index) : MiniscriptError
		{
			public override string ToString() => $"Incomplete expression at index {Index}";
		}

		public record ExpectedInteger(int Index) : UnexpectedToken(Index)
		{
			public override string ToString() => base.ToString() + " (Expected a positive integer)";
		}
		public record UnexpectedToken(int Index) : MiniscriptError
		{
			public override string ToString() => $"Unexpected token at index {Index}";
		}
		public record HashExpected(int Index, int RequiredBytes) : MiniscriptError
		{
			public override string ToString() => $"Hash of length {RequiredBytes} is expected at index {Index}";
		}
		public record LocktimeExpected(int Index) : MiniscriptError
		{
			public override string ToString() => $"Locktime expected at index {Index}";
		}
		public record UnsupportedFragment(int Index, KeyType KeyType) : MiniscriptError
		{
			public override string ToString() => $"Only fragments using KeyType {KeyType} are supported in this context";
		}
		public record KeyExpected(int Index, KeyType? Type) : MiniscriptError
		{
			public override string ToString() =>
				Type switch
				{
					KeyType.Taproot => $"Taproot PubKey expected at index {Index} (32 bytes)",
					KeyType.Classic => $"PubKey expected at index {Index} (33 bytes)",
					_ => $"Key expected at index {Index} (33 or 32 bytes)"
				};
		}
		public record UnknownFragmentName(int Index, string FragmentName) : MiniscriptError
		{
			public override string ToString() => $"Unknown fragment name '{FragmentName}' at index {Index}";
		}
		public record TooManyParameters(int Index, int Expected) : MiniscriptError
		{
			public override string ToString() => $"Too many parameters at index {Index}, expected {Expected}";
		}
		public record TooFewParameters(int Index, int Expected) : MiniscriptError
		{
			public override string ToString() => $"Too few parameters at index {Index}, expected {Expected}";
		}
		public record InvalidWrapper(char wrapper, int Index) : MiniscriptError
		{
			public override string ToString() => $"Invalid wrapper '{wrapper}' at index {Index}";
		}
	}
	public class MiniscriptFormatException : FormatException
	{
		public MiniscriptFormatException(MiniscriptError error) : base(error.ToString())
		{
			Error = error;
		}
		public MiniscriptError Error { get; }
	}
}
#endif
